Skip to content

fix: acra-backup export writes wrong data, zeroizes key before write, leaks key to logs#737

Open
zenmpi wants to merge 1 commit intocossacklabs:masterfrom
zenmpi:fix/acra-backup-export-bugs
Open

fix: acra-backup export writes wrong data, zeroizes key before write, leaks key to logs#737
zenmpi wants to merge 1 commit intocossacklabs:masterfrom
zenmpi:fix/acra-backup-export-bugs

Conversation

@zenmpi
Copy link

@zenmpi zenmpi commented Feb 26, 2026

Summary

Three critical security vulnerabilities in acra-backup --action=export (cmd/acra-backup/acra-backup.go lines 149-161).

Vuln 1: Raw master key written to backup file (CWE-312: Cleartext Storage of Sensitive Information)

os.WriteFile(file, backup.Keys, ...) writes the raw master key (32 bytes) to the backup file instead of backup.Data (the encrypted key bundle).

The Export() function (keystore/filesystem/filesystem_backup.go:331) returns:

return &keystore.KeysBackup{Data: encryptedKeys, Keys: newMasterKey}

But the import path (acra-backup.go:139-144) expects the file to contain Data:

keysContent, err := os.ReadFile(file)
backup := keystore.KeysBackup{Keys: key, Data: keysContent}

Export writes Keys to file, import reads the file into Data — the raw master key is stored on disk in cleartext and the export/import cycle is completely broken.

Vuln 2: Premature key zeroization — backup file contains null bytes

utils.ZeroizeSymmetricKey(backup.Keys) on line 156 zeros the slice in-place before os.WriteFile on line 157. Since Go slices share the underlying array, WriteFile receives a buffer filled with 32 null bytes. The backup file is silently corrupted with no error indication.

Vuln 3: Master key leaked to log aggregation (CWE-532: Insertion of Sensitive Information into Log File)

log.Infof("Backup master key: %s\n Backup saved to file: %s", base64MasterKey, file) sends the full base64-encoded master key through the logrus logging framework, which may forward to syslog, ELK, CloudWatch, Datadog, Splunk, or container log drivers (Docker, Kubernetes). The key persists in log storage indefinitely with no rotation mechanism.

PoC test output

=== RUN   TestVuln1_RawMasterKeyWrittenToFile
    poc_test.go:42: VULN CONFIRMED: import reads master key
    ("this-is-32-byte-master-key-12345") as Data instead of
    encrypted bundle ("encrypted-keystore-bundle-much-longer-data-here")
--- FAIL: TestVuln1_RawMasterKeyWrittenToFile

=== RUN   TestVuln2_ZeroizeBeforeWrite
    poc_test.go:65: VULN CONFIRMED: WriteFile would write 32 zero bytes
    instead of master key "this-is-32-byte-master-key-12345"
    poc_test.go:69: backupKeys after zeroize:
    [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
    poc_test.go:70: fileContent after zeroize:
    [0 0 0 0 0 0 0 0 ...] (same underlying array: true)
--- FAIL: TestVuln2_ZeroizeBeforeWrite

=== RUN   TestVuln3_MasterKeyInLogOutput
    poc_test.go:86: VULN CONFIRMED: master key fully recoverable from log output.
    Log line: "Backup master key: dGhpcy1pcy0zMi1ieXR...9F2B\n
    Backup saved to file: /tmp/backup.dat"
    Recovered key: "this-is-32-byte-master-key-12345"
--- FAIL: TestVuln3_MasterKeyInLogOutput

Impact

Severity: Critical

The backup master key protects the encrypted key bundle (backup.Data), which contains:

  • Client storage private keys (asymmetric decryption keys for AcraStructs/AcraBlocks)
  • Poison record keys (intrusion detection keypairs)
  • HMAC keys (searchable encryption)

These private keys decrypt all data protected by Acra in the database.

Attack scenario (Vuln 3): Any actor with read access to logs (SOC analyst, log aggregation service, compromised ELK/Splunk instance, shared CI/CD output) can recover the master key in base64 during an acra-backup export operation. With the master key, the attacker decrypts the backup bundle and gains access to all client encryption keys — full compromise of the cryptographic envelope.

Data loss scenario (Vuln 1 + 2): The export/import cycle is completely broken. The file written by export contains either 32 null bytes (due to premature zeroization) or the raw master key (wrong field), neither of which is valid input for import. Users who rely on acra-backup for disaster recovery have no functional backup.

The replacement tool acra-keys export does not have these issues — it writes Data and Keys to separate files with 0600 permissions and does not log key material.

Fix

Five changes in the case actionExport: block:

# Vulnerability Before After
1 CWE-312: cleartext key on disk os.WriteFile(file, backup.Keys, ...) os.WriteFile(file, backup.Data, ...) — write encrypted bundle
2 Premature zeroization ZeroizeSymmetricKey before WriteFile WriteFile first, then encode, then ZeroizeSymmetricKey
3 CWE-532: key in logs log.Infof("Backup master key: %s", ...) fmt.Fprintf(os.Stderr, "export %s=%s", ...) — bypass logging framework
4 Key material in immutable string base64MasterKey as string encodedKey as []byte — mutable, can be zeroed
5 Incomplete zeroization utils.ZeroizeBytes(encodedKey) after Fprintf — full zeroize chain

The output format export BACKUP_MASTER_KEY=<base64> matches what actionImport expects via os.Getenv(BackupMasterKeyVarName) (line 124).

fmt is already imported (line 22). No new dependencies.

Diff

-		base64MasterKey := base64.StdEncoding.EncodeToString(backup.Keys)
-		utils.ZeroizeSymmetricKey(backup.Keys)
-		if err := os.WriteFile(file, backup.Keys, filesystem.PrivateFileMode); err != nil {
+		if err := os.WriteFile(file, backup.Data, filesystem.PrivateFileMode); err != nil {
 			log.WithError(err).Errorf("Can't write backup to file %s", file)
 			os.Exit(1)
 		}
-		log.Infof("Backup master key: %s\n Backup saved to file: %s", base64MasterKey, file)
+		encodedKey := []byte(base64.StdEncoding.EncodeToString(backup.Keys))
+		utils.ZeroizeSymmetricKey(backup.Keys)
+		fmt.Fprintf(os.Stderr, "Export master key (required for import):\nexport %s=%s\n",
+			BackupMasterKeyVarName, encodedKey)
+		utils.ZeroizeBytes(encodedKey)
+		log.Infof("Backup saved to file: %s", file)

Verification

  1. Built Themis 0.15.0 from source (github.com/cossacklabs/themis)
  2. Compiled acra-backup binary successfully — go build ./cmd/acra-backup/
  3. Wrote and ran export→import round-trip integration test:
    • Export generates KeysBackup with Data (encrypted bundle) + Keys (master key)
    • WriteFile writes backup.Data to file — verified file is not all zeros
    • base64-encode backup.Keys into []byte, then ZeroizeSymmetricKey — verified all bytes are 0
    • Fprintf to stderr, then ZeroizeBytes(encodedKey) — verified all bytes are 0
    • Import reads file into Data, decodes base64 env var into Keys, calls backuper.Import
    • Result: 2 keys exported and imported successfully — full round-trip confirmed
=== RUN   TestExportImportRoundTrip
    acra_backup_export_fix_test.go:127: round-trip OK: exported and imported 2 keys
--- PASS: TestExportImportRoundTrip (0.01s)
PASS

Test plan

  • go build ./cmd/acra-backup/ compiles without errors
  • Export writes encrypted bundle (backup.Data) to file, not raw master key
  • File content is not all zeros (write happens before zeroize)
  • Exported file imports back successfully (full round-trip)
  • Master key does not appear in logrus output
  • Master key printed to stderr only, in export BACKUP_MASTER_KEY=<b64> format
  • backup.Keys (raw key bytes) zeroed after base64 encode
  • encodedKey (base64 []byte) zeroed after stderr write
  • No key material remains in memory after export completes

… leaks key to logs

Three bugs in acra-backup --action=export:

1. WriteFile used backup.Keys (raw master key) instead of backup.Data
   (encrypted bundle), making export/import incompatible (CWE-312)
2. ZeroizeSymmetricKey was called before WriteFile, so the file
   received 32 null bytes (Go slices share underlying array)
3. Master key was logged via log.Infof, exposing it to log aggregation
   systems (CWE-532)

Fix: write backup.Data, move zeroize after write+encode, output key
via fmt.Fprintf(os.Stderr) formatted as export BACKUP_MASTER_KEY=<b64>
to match what import expects from the env var.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant